// ========================================================================
// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================

package org.eclipse.jetty.webapp;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.servlet.Servlet;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.xml.XmlParser;

/* ------------------------------------------------------------ */
/**
 * TagLibConfiguration. The class searches for TLD descriptors found in web.xml, in WEB-INF/*.tld files of the web app or *.tld files within jars found in WEB-INF/lib of the webapp. Any listeners defined in these tld's are added to the context. &lt;bile&gt;This is total rubbish special case for JSPs! If there was a general use-case for web app frameworks to register listeners directly, then a generic mechanism could have been added to the servlet spec. Instead some special purpose JSP support is required that breaks all sorts of encapsulation rules as the servlet container must go searching for and then parsing the descriptors for one particular framework. It only appears to be used by JSF, which is being developed by the same developer who implemented this feature in the first place! &lt;/bile&gt; Note- this has been superceded by the new TldScanner in jasper which uses ServletContainerInitializer to find all the listeners in tag libs and register them.
 */
public class TagLibConfiguration extends AbstractConfiguration
{

	private static final Logger LOG = Log.getLogger(TagLibConfiguration.class);

	public static final String TLD_RESOURCES = "org.eclipse.jetty.tlds";

	/**
	 * TagLibListener A listener that does the job of finding .tld files that contain (other) listeners that need to be called by the servlet container. This implementation is necessitated by the fact that it is only after all the Configuration classes have run that we will parse web.xml/fragments etc and thus find tlds mentioned therein. Note: TagLibConfiguration is not used in jetty-8 as jasper (JSP engine) uses the new TldScanner class - a ServletContainerInitializer from Servlet Spec 3 - to find all listeners in taglibs and register them with the servlet container.
	 */
	public class TagLibListener implements ServletContextListener {

		private List<EventListener> _tldListeners;
		private WebAppContext _context;

		public TagLibListener(WebAppContext context) {
			_context = context;
		}

		public void contextDestroyed(ServletContextEvent sce)
		{
			if (_tldListeners == null)
				return;

			for (int i = _tldListeners.size() - 1; i >= 0; i--) {
				EventListener l = _tldListeners.get(i);
				if (l instanceof ServletContextListener) {
					((ServletContextListener)l).contextDestroyed(sce);
				}
			}
		}

		public void contextInitialized(ServletContextEvent sce)
		{
			try {
				//find the tld files and parse them to get out their
				//listeners
				Set<Resource> tlds = findTldResources();
				List<TldDescriptor> descriptors = parseTlds(tlds);
				processTlds(descriptors);

				if (_tldListeners == null)
					return;

				//call the listeners that are ServletContextListeners, put the
				//rest into the context's list of listeners to call at the appropriate
				//moment
				for (EventListener l: _tldListeners) {
					if (l instanceof ServletContextListener) {
						((ServletContextListener)l).contextInitialized(sce);
					} else {
						_context.addEventListener(l);
					}
				}

			} catch (Exception e) {
				LOG.warn(e);
			}
		}

		/**
		 * Find all the locations that can harbour tld files that may contain a listener which the web container is supposed to instantiate and call.
		 * 
		 * @return
		 * @throws IOException
		 */
		private Set<Resource> findTldResources() throws IOException {

			Set<Resource> tlds = new HashSet<Resource>();

			// Find tld's from web.xml
			// When web.xml was processed, it should have created aliases for all TLDs.  So search resources aliases
			// for aliases ending in tld
			if (_context.getResourceAliases() != null &&
				_context.getBaseResource() != null &&
				_context.getBaseResource().exists())
			{
				Iterator<String> iter = _context.getResourceAliases().values().iterator();
				while (iter.hasNext())
				{
					String location = iter.next();
					if (location != null && location.toLowerCase().endsWith(".tld"))
					{
						if (!location.startsWith("/"))
							location = "/WEB-INF/" + location;
						Resource l = _context.getBaseResource().addPath(location);
						tlds.add(l);
					}
				}
			}

			// Look for any tlds in WEB-INF directly.
			Resource web_inf = _context.getWebInf();
			if (web_inf != null)
			{
				String[] contents = web_inf.list();
				for (int i = 0; contents != null && i < contents.length; i++)
				{
					if (contents[i] != null && contents[i].toLowerCase().endsWith(".tld"))
					{
						Resource l = web_inf.addPath(contents[i]);
						tlds.add(l);
					}
				}
			}

			//Look for tlds in common location of WEB-INF/tlds
			if (web_inf != null) {
				Resource web_inf_tlds = _context.getWebInf().addPath("/tlds/");
				if (web_inf_tlds.exists() && web_inf_tlds.isDirectory()) {
					String[] contents = web_inf_tlds.list();
					for (int i = 0; contents != null && i < contents.length; i++)
					{
						if (contents[i] != null && contents[i].toLowerCase().endsWith(".tld"))
						{
							Resource l = web_inf_tlds.addPath(contents[i]);
							tlds.add(l);
						}
					}
				}
			}

			// Add in tlds found in META-INF of jars. The jars that will be scanned are controlled by
			// the patterns defined in the context attributes: org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern,
			// and org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern
			@SuppressWarnings("unchecked")
			Collection<Resource> tld_resources = (Collection<Resource>)_context.getAttribute(TLD_RESOURCES);
			if (tld_resources != null)
				tlds.addAll(tld_resources);

			return tlds;
		}

		/**
		 * Parse xml into in-memory tree
		 * 
		 * @param tlds
		 * @return
		 */
		private List<TldDescriptor> parseTlds(Set<Resource> tlds) {
			List<TldDescriptor> descriptors = new ArrayList<TldDescriptor>();

			Resource tld = null;
			Iterator<Resource> iter = tlds.iterator();
			while (iter.hasNext())
			{
				try
				{
					tld = iter.next();
					if (LOG.isDebugEnabled())
						LOG.debug("TLD=" + tld);

					TldDescriptor d = new TldDescriptor(tld);
					d.parse();
					descriptors.add(d);
				} catch (Exception e)
				{
					LOG.warn("Unable to parse TLD: " + tld, e);
				}
			}
			return descriptors;
		}

		/**
		 * Create listeners from the parsed tld trees
		 * 
		 * @param descriptors
		 * @throws Exception
		 */
		private void processTlds(List<TldDescriptor> descriptors) throws Exception {

			TldProcessor processor = new TldProcessor();
			for (TldDescriptor d: descriptors)
				processor.process(_context, d);

			_tldListeners = new ArrayList<EventListener>(processor.getListeners());
		}
	}

	/**
	 * TldDescriptor
	 */
	public static class TldDescriptor extends Descriptor
	{

		protected static XmlParser __parserSingleton;

		public TldDescriptor(Resource xml)
		{
			super(xml);
		}

		@Override
		public void ensureParser() throws ClassNotFoundException
		{
			if (__parserSingleton == null)
				__parserSingleton = newParser();
			_parser = __parserSingleton;
		}

		@Override
		public XmlParser newParser() throws ClassNotFoundException
		{
			// Create a TLD parser
			XmlParser parser = new XmlParser(false);

			URL taglib11 = null;
			URL taglib12 = null;
			URL taglib20 = null;
			URL taglib21 = null;

			try
			{
				Class<?> jsp_page = Loader.loadClass(WebXmlConfiguration.class, "javax.servlet.jsp.JspPage");
				taglib11 = jsp_page.getResource("javax/servlet/jsp/resources/web-jsptaglibrary_1_1.dtd");
				taglib12 = jsp_page.getResource("javax/servlet/jsp/resources/web-jsptaglibrary_1_2.dtd");
				taglib20 = jsp_page.getResource("javax/servlet/jsp/resources/web-jsptaglibrary_2_0.xsd");
				taglib21 = jsp_page.getResource("javax/servlet/jsp/resources/web-jsptaglibrary_2_1.xsd");
			} catch (Exception e)
			{
				LOG.ignore(e);
			} finally
			{
				if (taglib11 == null)
					taglib11 = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/web-jsptaglibrary_1_1.dtd", true);
				if (taglib12 == null)
					taglib12 = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/web-jsptaglibrary_1_2.dtd", true);
				if (taglib20 == null)
					taglib20 = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/web-jsptaglibrary_2_0.xsd", true);
				if (taglib21 == null)
					taglib21 = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/web-jsptaglibrary_2_1.xsd", true);
			}

			if (taglib11 != null)
			{
				redirect(parser, "web-jsptaglib_1_1.dtd", taglib11);
				redirect(parser, "web-jsptaglibrary_1_1.dtd", taglib11);
			}
			if (taglib12 != null)
			{
				redirect(parser, "web-jsptaglib_1_2.dtd", taglib12);
				redirect(parser, "web-jsptaglibrary_1_2.dtd", taglib12);
			}
			if (taglib20 != null)
			{
				redirect(parser, "web-jsptaglib_2_0.xsd", taglib20);
				redirect(parser, "web-jsptaglibrary_2_0.xsd", taglib20);
			}
			if (taglib21 != null)
			{
				redirect(parser, "web-jsptaglib_2_1.xsd", taglib21);
				redirect(parser, "web-jsptaglibrary_2_1.xsd", taglib21);
			}

			parser.setXpath("/taglib/listener/listener-class");
			return parser;
		}

		@Override
		public void parse()
			throws Exception
		{
			ensureParser();
			try
			{
				//xerces on apple appears to sometimes close the zip file instead
				//of the inputstream, so try opening the input stream, but if
				//that doesn't work, fallback to opening a new url
				_root = _parser.parse(_xml.getInputStream());
			} catch (Exception e)
			{
				_root = _parser.parse(_xml.getURL().toString());
			}

			if (_root == null)
			{
				LOG.warn("No TLD root in {}", _xml);
			}
		}
	}

	/**
	 * TldProcessor Process TldDescriptors representing tag libs to find listeners.
	 */
	public class TldProcessor extends IterativeDescriptorProcessor
	{

		public static final String TAGLIB_PROCESSOR = "org.eclipse.jetty.tagLibProcessor";
		XmlParser _parser;
		List<XmlParser.Node> _roots = new ArrayList<XmlParser.Node>();
		List<EventListener> _listeners;

		public TldProcessor()
			throws Exception
		{
			_listeners = new ArrayList<EventListener>();
			registerVisitor("listener", this.getClass().getDeclaredMethod("visitListener", __signature));
		}

		public void visitListener(WebAppContext context, Descriptor descriptor, XmlParser.Node node)
		{
			String className = node.getString("listener-class", false, true);
			if (LOG.isDebugEnabled())
				LOG.debug("listener=" + className);

			try
			{
				Class<?> listenerClass = context.loadClass(className);
				EventListener l = (EventListener)listenerClass.newInstance();
				_listeners.add(l);
			} catch (Exception e)
			{
				LOG.warn("Could not instantiate listener " + className + ": " + e);
				LOG.debug(e);
			} catch (Error e)
			{
				LOG.warn("Could not instantiate listener " + className + ": " + e);
				LOG.debug(e);
			}

		}

		@Override
		public void end(WebAppContext context, Descriptor descriptor)
		{}

		@Override
		public void start(WebAppContext context, Descriptor descriptor)
		{}

		public List<EventListener> getListeners() {
			return _listeners;
		}
	}

	@Override
	public void preConfigure(WebAppContext context) throws Exception
	{
		try
		{
			@SuppressWarnings("unused")
			Class<?> jsp_page = Loader.loadClass(WebXmlConfiguration.class, "javax.servlet.jsp.JspPage");
		} catch (Exception e)
		{
			//no jsp available, don't parse TLDs
			return;
		}

		TagLibListener tagLibListener = new TagLibListener(context);
		context.addEventListener(tagLibListener);
	}

	@Override
	public void configure(WebAppContext context) throws Exception
	{}

	@Override
	public void postConfigure(WebAppContext context) throws Exception
	{}

	@Override
	public void cloneConfigure(WebAppContext template, WebAppContext context) throws Exception
	{}

	@Override
	public void deconfigure(WebAppContext context) throws Exception
	{}
}
